package org.hugoduncan.appam.servlet;

import java.io.IOException;
import java.util.Locale;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hivemind.ClassResolver;
import org.apache.hivemind.ErrorHandler;
import org.apache.hivemind.Registry;
import org.apache.hivemind.Resource;
import org.apache.hivemind.impl.DefaultClassResolver;
import org.apache.hivemind.impl.RegistryBuilder;
import org.apache.hivemind.impl.StrictErrorHandler;
import org.apache.hivemind.impl.XmlModuleDescriptorProvider;
import org.apache.hivemind.util.ContextResource;
import org.hugoduncan.appam.AppamMessages;
import org.hugoduncan.appam.server.AppServletResponse;
import org.hugoduncan.appam.server.ApplicationInitializer;
import org.hugoduncan.appam.server.Manager;
import org.hugoduncan.appam.server.impl.AppServletRequestImpl;
import org.hugoduncan.appam.server.impl.AppServletResponseImpl;

/**
 * Servlet implementation class for Servlet: ApplicationServlet
 *
 * Heavily inspired by Tapestry's ApplicationServlet
 */
public class ApplicationServlet
    extends javax.servlet.http.HttpServlet
    implements javax.servlet.Servlet
{
    private static final long serialVersionUID = 1L;

    /**
     * Prefix used to store the HiveMind Registry into the
     * ServletContext. This string is suffixed with the servlet name
     * (in case multiple Appam applications are executing within a
     * single web application).
     */
    private static final String REGISTRY_KEY_PREFIX =
        "org.hugoduncan.appam.Registry:";

    private static final Log log = LogFactory.getLog(ApplicationServlet.class);

    private ClassResolver classResolver;

    /**
     * The key used to store the registry into the ServletContext.
     */
    private String registryKey;

    private Registry registry;

    private Manager manager;

    /**
     * Handles the GET and POST requests. Performs the following:
     * <ul>
     * <li>Construct a {@link RequestContext}
     * <li>Invoke {@link #getEngine(RequestContext)}to get or create the {@link IEngine}
     * <li>Invoke {@link IEngine#service(RequestContext)}on the application
     * </ul>
     */

    @Override protected void service(HttpServletRequest request,
                                     HttpServletResponse response)
        throws IOException, ServletException
    {
        try
        {
            registry.setupThread();
            AppServletResponse appResponse=new AppServletResponseImpl(response);
            appResponse.setBaseUrl(getBaseUrl(request));
            if (!manager.process(new AppServletRequestImpl(request),appResponse))
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Unknown resource");
        }
        catch (ServletException ex)
        {
            log("ServletException", ex);
            // Rethrow it.
            throw ex;
        }
        catch (IOException ex)
        {
            log("IOException", ex);
            // Rethrow it.
            throw ex;
        }
        catch (Exception ex)
        {
            log("Exception", ex);
            // Rethrow it.
            throw new ServletException(ex);
        }
        finally
        {
            registry.cleanupThread();
        }
    }

    private String getBaseUrl(HttpServletRequest request) {
        String url=request.getRequestURL().toString();
        String match=request.getContextPath()+request.getServletPath();
        return url.substring(0,url.indexOf(match)+match.length());
    }

    /**
     * Reads the application specification when the servlet is first
     * initialized. All {@link IEngine engine instances} will have
     * access to the specification via the servlet.
     *
     * @see #constructApplicationSpecification()
     * @see #createResourceResolver()
     */
    public void init(ServletConfig config) throws ServletException
    {
        // construct the registry key
        String name = config.getServletName();
        registryKey = REGISTRY_KEY_PREFIX + name;

        long startTime = System.currentTimeMillis();
        long elapsedToRegistry = 0;

        super.init(config);

        classResolver = createClassResolver();

        try
        {
            registry = constructRegistry(config);
            elapsedToRegistry = System.currentTimeMillis() - startTime;
            initializeApplication();
            config.getServletContext().setAttribute(registryKey, registry);
        }
        catch (Exception ex)
        {
            log("Unexpected exception",ex);
            Throwable t=ex;
            while (t.getCause()!=null)
            {
                log.info("Caused by: ", ex.getCause());
                t=ex.getCause();
            }
            throw new ServletException(
                AppamMessages.servletInitFailure(ex), ex);
        }

        long elapsedOverall = System.currentTimeMillis() - startTime;
        log.info(AppamMessages.servletInit(
                     name, elapsedToRegistry, elapsedOverall));
    }

    protected ClassResolver createClassResolver()
    {
        return new DefaultClassResolver();
    }

    /**
     * Invoked from {@link #init(ServletConfig)}to construct the
     * Registry to be used by the application.  <p> This looks in the
     * standard places (on the classpath), but also in the
     * WEB-INF/name and WEB-INF folders (where name is the name of the
     * servlet).
     */
    protected Registry constructRegistry(ServletConfig config)
    {
        ErrorHandler errorHandler = constructErrorHandler(config);
        RegistryBuilder builder = new RegistryBuilder(errorHandler);
        builder.addModuleDescriptorProvider(
            new XmlModuleDescriptorProvider(classResolver));

        String name = config.getServletName();
        ServletContext context = config.getServletContext();

        addModuleIfExists(builder, context,
                          "/WEB-INF/" + name + "/hivemodule.xml");
        addModuleIfExists(builder, context,
                          "/WEB-INF/hivemodule.xml");

        return builder.constructRegistry(Locale.getDefault());
    }

    /**
     * Invoked by {@link #constructRegistry(ServletConfig)} to create
     * and return an {@link ErrorHandler} instance to be used when
     * constructing the Registry (and then to handle any runtime
     * exceptions). This implementation returns a new instance of
     * {@link org.apache.hivemind.impl.StrictErrorHandler}.
     *
     * @since 4.0
     */
    protected ErrorHandler constructErrorHandler(ServletConfig config)
    {
        return new StrictErrorHandler();
    }

    /**
     * Looks for a file in the servlet context; if it exists, it is
     * expected to be a HiveMind module descriptor, and is added to
     * the builder.
     *
     * @since 4.0
     */
    protected void addModuleIfExists(
        RegistryBuilder builder, ServletContext context, String path)
    {
        Resource r = new ContextResource(context, path);

        if (r.getResourceURL() == null)
            return;

        builder.addModuleDescriptorProvider(
            new XmlModuleDescriptorProvider(classResolver, r));
    }

    /**
     * Invoked from {@link #init(ServletConfig)}, after the registry
     * has been constructed, to bootstrap the application via the
     * <code>tapestry.MasterApplicationInitializer</code> service.
     */
    protected void initializeApplication()
    {
        ApplicationInitializer ai =
            (ApplicationInitializer) registry.getService(
                "appam.init.MasterInitializer",
                ApplicationInitializer.class);

        ai.initialize(this);

        registry.cleanupThread();

        manager = (Manager) registry.getService(
                "appam.server.Manager",
                Manager.class);
    }

    /**
     * Shuts down the registry (if it exists).
     */
    public void destroy()
    {
        getServletContext().removeAttribute(registryKey);

        if (registry != null)
        {
            registry.shutdown();
            registry = null;
        }
        super.destroy();
    }

}